package com.nulldev.util.VariableAPI;

import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.util.random.GlobalRandom;
import com.nulldev.util.VariableAPI.util.strings.stringsearch.BNDM;
import com.nulldev.util.VariableAPI.util.strings.stringsearch.BNDMCI;
import com.nulldev.util.VariableAPI.util.strings.stringsearch.BoyerMooreHorspool;
import com.nulldev.util.VariableAPI.util.strings.stringsearch.BoyerMooreHorspoolRaita;
import com.nulldev.util.VariableAPI.util.strings.stringsearch.StringSearch;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.internal.backport.concurrency9.Lists;

public class StringsUtil {
	private static IllegalStateException newIllegalStateException(final String charsetName, final UnsupportedEncodingException e) {
		return new IllegalStateException(charsetName + ": " + e);
	}

	public static String capitalizeString(String string) {
		if (string == null || string.isEmpty()) {
			return string;
		}
		char[] chars = string.toLowerCase().toCharArray();
		boolean found = false;
		for (int i = 0; i < chars.length; i++) {
			if (!found && Character.isLetter(chars[i])) {
				chars[i] = Character.toUpperCase(chars[i]);
				found = true;
			} else if (Character.isWhitespace(chars[i]) || chars[i] == '.' || chars[i] == '\'') {
				found = false;
			}
		}
		return String.valueOf(chars);
	}

	public static String _capitalizeString(String string) {
		if (string == null || string.isEmpty()) {
			return string;
		}
		return string.substring(0, 1).toUpperCase() + string.substring(1);
	}

	public static class Split {
		public static String[] split(final String str) {
			return split(str, null, -1);
		}

		public static String[] split(final String str, final char separatorChar) {
			return splitWorker(str, separatorChar, false);
		}

		public static String[] split(final String str, final String separatorChars) {
			return splitWorker(str, separatorChars, -1, false);
		}

		public static String[] split(final String str, final String separatorChars, final int max) {
			return splitWorker(str, separatorChars, max, false);
		}

		public static String[] splitByWholeSeparator(final String str, final String separator) {
			return splitByWholeSeparatorWorker(str, separator, -1, false);
		}

		public static String[] splitByWholeSeparator(final String str, final String separator, final int max) {
			return splitByWholeSeparatorWorker(str, separator, max, false);
		}

		public static String[] splitByWholeSeparatorPreserveAllTokens(final String str, final String separator) {
			return splitByWholeSeparatorWorker(str, separator, -1, true);
		}

		public static String[] splitByWholeSeparatorPreserveAllTokens(final String str, final String separator, final int max) {
			return splitByWholeSeparatorWorker(str, separator, max, true);
		}

		private static String[] splitByWholeSeparatorWorker(final String str, final String separator, final int max, final boolean preserveAllTokens) {
			if (str == null) {
				return null;
			}
			final int len = str.length();
			if (len == 0) {
				return (String[]) ArrayUtils.EMPTY_STRING_ARRAY;
			}
			if (separator == null || "".equals(separator)) {
				return splitWorker(str, null, max, preserveAllTokens);
			}
			final int separatorLength = separator.length();
			final ArrayList<String> substrings = new ArrayList<String>();
			int numberOfSubstrings = 0;
			int beg = 0;
			int end = 0;
			while (end < len) {
				end = str.indexOf(separator, beg);
				if (end > -1) {
					if (end > beg) {
						if (++numberOfSubstrings == max) {
							end = len;
							substrings.add(str.substring(beg));
						} else {
							substrings.add(str.substring(beg, end));
							beg = end + separatorLength;
						}
					} else {
						if (preserveAllTokens) {
							if (++numberOfSubstrings == max) {
								end = len;
								substrings.add(str.substring(beg));
							} else {
								substrings.add("");
							}
						}
						beg = end + separatorLength;
					}
				} else {
					substrings.add(str.substring(beg));
					end = len;
				}
			}
			return substrings.toArray(new String[substrings.size()]);
		}

		private static String[] splitWorker(final String str, final char separatorChar, final boolean preserveAllTokens) {
			if (str == null) {
				return null;
			}
			final int len = str.length();
			if (len == 0) {
				return (String[]) ArrayUtils.EMPTY_STRING_ARRAY;
			}
			final List<String> list = new ArrayList<String>();
			int i = 0;
			int start = 0;
			boolean match = false;
			boolean lastMatch = false;
			while (i < len) {
				if (str.charAt(i) == separatorChar) {
					if (match || preserveAllTokens) {
						list.add(str.substring(start, i));
						match = false;
						lastMatch = true;
					}
					start = ++i;
				} else {
					lastMatch = false;
					match = true;
					++i;
				}
			}
			if (match || (preserveAllTokens && lastMatch)) {
				list.add(str.substring(start, i));
			}
			return list.toArray(new String[list.size()]);
		}

		private static String[] splitWorker(final String str, final String separatorChars, final int max, final boolean preserveAllTokens) {
			if (str == null) {
				return null;
			}
			final int len = str.length();
			if (len == 0) {
				return (String[]) ArrayUtils.EMPTY_STRING_ARRAY;
			}
			final List<String> list = new ArrayList<String>();
			int sizePlus1 = 1;
			int i = 0;
			int start = 0;
			boolean match = false;
			boolean lastMatch = false;
			if (separatorChars == null) {
				while (i < len) {
					if (Character.isWhitespace(str.charAt(i))) {
						if (match || preserveAllTokens) {
							lastMatch = true;
							if (sizePlus1++ == max) {
								i = len;
								lastMatch = false;
							}
							list.add(str.substring(start, i));
							match = false;
						}
						start = ++i;
					} else {
						lastMatch = false;
						match = true;
						++i;
					}
				}
			} else if (separatorChars.length() == 1) {
				final char sep = separatorChars.charAt(0);
				while (i < len) {
					if (str.charAt(i) == sep) {
						if (match || preserveAllTokens) {
							lastMatch = true;
							if (sizePlus1++ == max) {
								i = len;
								lastMatch = false;
							}
							list.add(str.substring(start, i));
							match = false;
						}
						start = ++i;
					} else {
						lastMatch = false;
						match = true;
						++i;
					}
				}
			} else {
				while (i < len) {
					if (separatorChars.indexOf(str.charAt(i)) >= 0) {
						if (match || preserveAllTokens) {
							lastMatch = true;
							if (sizePlus1++ == max) {
								i = len;
								lastMatch = false;
							}
							list.add(str.substring(start, i));
							match = false;
						}
						start = ++i;
					} else {
						lastMatch = false;
						match = true;
						++i;
					}
				}
			}
			if (match || (preserveAllTokens && lastMatch)) {
				list.add(str.substring(start, i));
			}
			return list.toArray(new String[list.size()]);
		}
	}

	public static class NewStrings {

		public static String newString(final byte[] bytes, final String charsetName) {
			if (bytes == null) {
				return null;
			}
			try {
				return new String(bytes, charsetName);
			} catch (UnsupportedEncodingException e) {
				throw newIllegalStateException(charsetName, e);
			}
		}

		public static String newString(final byte[] bytes, final Charset charset) {
			return (bytes == null) ? null : new String(bytes, charset);
		}

		public static String newString(final byte[] bytes) {
			return (bytes == null) ? null : new String(bytes);
		}

		public static String newStringIso8859_1(final byte[] bytes) {
			return newString(bytes, CharsetUtil.ISO_8859_1);
		}

		public static String newStringUsAscii(final byte[] bytes) {
			return newString(bytes, CharsetUtil.US_ASCII);
		}

		public static String newStringUtf16(final byte[] bytes) {
			return newString(bytes, CharsetUtil.UTF_16);
		}

		public static String newStringUtf16Be(final byte[] bytes) {
			return newString(bytes, CharsetUtil.UTF_16BE);
		}

		public static String newStringUtf16Le(final byte[] bytes) {
			return newString(bytes, CharsetUtil.UTF_16LE);
		}

		public static String newStringUtf8(final byte[] bytes) {
			return newString(bytes, CharsetUtil.UTF_8);
		}

		public static String toString(final byte[] toString) {
			return newString(toString, CharsetUtil.getCharset());
		}

		public static String toString(final byte[] toString, final Charset charset) {
			return newString(toString, charset);
		}
	}

	public static class GetBytes {

		public static byte[] getBytes(final String string, final Charset charset) {
			if (string == null) {
				return null;
			}
			return string.getBytes(charset);
		}

		public static byte[] getBytesIso8859_1(final String string) {
			return getBytes(string, CharsetUtil.ISO_8859_1);
		}

		public static byte[] getBytesUnchecked(final String string, final String charsetName) {
			if (string == null) {
				return null;
			}
			try {
				return string.getBytes(charsetName);
			} catch (UnsupportedEncodingException e) {
				throw newIllegalStateException(charsetName, e);
			}
		}

		public static byte[] getBytesUsAscii(final String string) {
			return getBytes(string, CharsetUtil.US_ASCII);
		}

		public static byte[] getBytesUtf16(final String string) {
			return getBytes(string, CharsetUtil.UTF_16);
		}

		public static byte[] getBytesUtf16Be(final String string) {
			return getBytes(string, CharsetUtil.UTF_16BE);
		}

		public static byte[] getBytesUtf16Le(final String string) {
			return getBytes(string, CharsetUtil.UTF_16LE);
		}

		public static byte[] getBytesUtf8(final String string) {
			return getBytes(string, CharsetUtil.UTF_8);
		}
	}

	private static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
	private static final String AB_ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
	private static final SecureRandom rnd = GlobalRandom.getGlobalInsecureRandom();

	public static String randomString(final int len) {
		if (len < 0)
			throw new IllegalArgumentException("len < 0!");
		final StringBuilder sb = new StringBuilder(len);
		for (int i = 0; i < len; i++)
			sb.append(AB.charAt(rnd.nextInt(AB.length())));
		return sb.toString();
	}

	public static String randomStringAlphabet(final int len) {
		if (len < 0)
			throw new IllegalArgumentException("len < 0!");
		final StringBuilder sb = new StringBuilder(len);
		for (int i = 0; i < len; i++)
			sb.append(AB_ALPHA.charAt(rnd.nextInt(AB_ALPHA.length())));
		return sb.toString();
	}

	public static String generateSecure(final int length) {
		return randomString(length);
	}

	public static String substringBetween(final String str, final String tag) {
		return substringBetween(str, tag, tag);
	}

	private static final boolean ENABLE_SEARCH_OPTS = Arguments.notArguments("--StringsUtil.disableSearchOpts") && JVM.version() < 17f;
	private static final int FAST_SEARCH_THRESHOLD = 2048;

	protected static enum SearchingWith {
		SEARCH_TYPE_CHARS,
		SEARCH_TYPE_BYTES,
		SEARCH_TYPE_STRING
	}

	private static StringSearch getAlgFor(final int len, final SearchingWith with) {
		return getAlgFor(len, with, false);
	}

	private static StringSearch getAlgFor(final int len, final SearchingWith with, final boolean caseSensitive) {
		// TODO: Tweak this to the max.
		if (len > 31) {
			switch (with) {
				case SEARCH_TYPE_CHARS:
				case SEARCH_TYPE_STRING:
					return new BoyerMooreHorspoolRaita();
				default:
					return new BoyerMooreHorspool();
			}
		} else if (caseSensitive) {
			return new BNDMCI();
		} else
			return new BNDM();
//		if (len >= 5 && len <= 31) return new ShiftOrMismatches();
	}

	public static String substringBetweenO(final String str, final String open, final String close) {
		if (str == null || open == null || close == null)
			return null;
		if (str.length() >= FAST_SEARCH_THRESHOLD && ENABLE_SEARCH_OPTS) {
			final StringSearch alg = getAlgFor((open.length() + close.length()) / 2, SearchingWith.SEARCH_TYPE_STRING);
			final int start = alg.searchString(str, open);
			if (start != -1) {
				final int end = alg.searchString(str, start + open.length(), close);
				if (end != -1)
					return str.substring(start + open.length(), end);
			}
		} else {
			final int start = str.indexOf(open);
			if (start != -1) {
				final int end = str.indexOf(close, start + open.length());
				if (end != -1)
					return str.substring(start + open.length(), end);
			}
		}
		return null;
	}

	public static String substringBetween(final String str, final String open, final String close) {
		if (str == null || open == null || close == null) {
			return str;
		}
		if (str.length() >= FAST_SEARCH_THRESHOLD && ENABLE_SEARCH_OPTS) {
			final StringSearch alg = getAlgFor((open.length() + close.length()) / 2, SearchingWith.SEARCH_TYPE_STRING);
			final int start = alg.searchString(str, open);
			if (start != -1) {
				final int end = alg.searchString(str, start + open.length(), close);
				if (end != -1)
					return str.substring(start + open.length(), end);
			}
		} else {
			final int start = str.indexOf(open);
			if (start != -1) {
				final int end = str.indexOf(close, start + open.length());
				if (end != -1)
					return str.substring(start + open.length(), end);
			}
		}
		return str;
	}

	public static String substringBetweenNA(final String str, final String open, final String close) {
		if (str == null || open == null || close == null) {
			return "N/A";
		}
		if (str.length() >= FAST_SEARCH_THRESHOLD && ENABLE_SEARCH_OPTS) {
			final StringSearch alg = getAlgFor((open.length() + close.length()) / 2, SearchingWith.SEARCH_TYPE_STRING);
			final int start = alg.searchString(str, open);
			if (start != -1) {
				final int end = alg.searchString(str, start + open.length(), close);
				if (end != -1)
					return str.substring(start + open.length(), end);
			}
		} else {
			final int start = str.indexOf(open);
			if (start != -1) {
				final int end = str.indexOf(close, start + open.length());
				if (end != -1)
					return str.substring(start + open.length(), end);
			}
		}
		return "N/A";
	}

	public static String substringBetweenF(final String str, final String open, final String close, final String fallback) {
		if (str == null || open == null || close == null) {
			return fallback;
		}
		if (str.length() >= FAST_SEARCH_THRESHOLD && ENABLE_SEARCH_OPTS) {
			final StringSearch alg = getAlgFor((open.length() + close.length()) / 2, SearchingWith.SEARCH_TYPE_STRING);
			final int start = alg.searchString(str, open);
			if (start != -1) {
				final int end = alg.searchString(str, start + open.length(), close);
				if (end != -1)
					return str.substring(start + open.length(), end);
			}
		} else {
			final int start = str.indexOf(open);
			if (start != -1) {
				final int end = str.indexOf(close, start + open.length());
				if (end != -1)
					return str.substring(start + open.length(), end);
			}
		}
		return fallback;
	}

	public static String[] byLineSepeartor(final String text) {
		return Variables.requireNonNullObject(text, "text").split(Pattern.quote(JVM.lineSeperator()));
	}

	public static String safeSplit(final String line) {
		final StringBuilder out = new StringBuilder();
		final char[] data = line.toCharArray();
		boolean delnext = false;
		boolean inquote = false;
		for (char x : data) {
			if (!delnext) {
				out.append(x);
			} else {
				delnext = false;
			}
			if (x == '"') {
				if (inquote) {
					out.append("__sp__");
					delnext = true;
				}
				inquote = !inquote;
			}
		}
		return out.toString();
	}

	public static boolean isEqualCM(final String[] data, final int index, final String search) {
		try {
			return data[index].replaceAll("\"", "").equals(search);
		} catch (Exception e) {
			return false;
		}
	}

	public static String limitSpaces(final String data, final int max) {
		final StringBuilder out = new StringBuilder();
		int spacesHit = 0;
		for (final char x : data.toCharArray()) {
			if (x == ' ') {
				if (spacesHit < max) {
					out.append(x);
				}
				spacesHit += 1;
			} else {
				out.append(x);
				spacesHit = 0;
			}
		}
		return out.toString();
	}

	public static String join(String[] array) {
		return join(Arrays.asList(array), ",");
	}

	public static String join(String[] array, final char c) {
		return join(Arrays.asList(array), new String(new char[]
			{ c }));
	}

	public static <T> String join(Enumeration<T> array, final char c) {
		return join(Arrays.asList(array), new String(new char[]
			{ c }));
	}

	public static String join(char[] array, final char c) {
		return join(Arrays.asList(array), new String(new char[]
			{ c }));
	}

	public static String join(final Iterable<?> iterable, final char separator) {
		if (iterable == null)
			return null;
		return join(iterable.iterator(), separator);
	}

	public static String join(final Iterable<?> iterable, final String separator) {
		if (iterable == null)
			return null;
		return join(iterable.iterator(), separator);
	}

	public static String join(final Iterator<?> iterator, final char separator) {
		if (iterator == null) {
			return null;
		}
		if (!iterator.hasNext()) {
			return "";
		}
		final Object first = iterator.next();
		if (!iterator.hasNext()) {
			return String.valueOf(first);
		}
		final StringBuilder buf = new StringBuilder(256);
		if (first != null) {
			buf.append(first);
		}
		while (iterator.hasNext()) {
			buf.append(separator);
			final Object obj = iterator.next();
			if (obj != null) {
				buf.append(obj);
			}
		}
		return buf.toString();
	}

	public static String join(final Iterator<?> iterator, final String separator) {
		if (iterator == null) {
			return null;
		}
		if (!iterator.hasNext()) {
			return "";
		}
		final Object first = iterator.next();
		if (!iterator.hasNext()) {
			return String.valueOf(first);
		}
		final StringBuilder buf = new StringBuilder(256);
		if (first != null) {
			buf.append(first);
		}
		while (iterator.hasNext()) {
			if (separator != null) {
				buf.append(separator);
			}
			final Object obj = iterator.next();
			if (obj != null) {
				buf.append(obj);
			}
		}
		return buf.toString();
	}

	public static String join(Collection<?> items, String delimiter) {
		return join(items, delimiter, -1, -1);
	}

	public static String join(Collection<?> items, String delimiter, int start) {
		return join(items, delimiter, start, -1);
	}

	public static String join(Collection<?> items, String delimiter, int start, int end) {
		if (items == null || items.isEmpty()) {
			return "";
		}
		start = start > -1 ? start : 0;
		end = end > -1 ? end : items.size();

		final StringBuilder b = new StringBuilder();
		final Iterator<?> it = items.iterator();
		int i = 0;
		while (it.hasNext()) {
			String next = it.next().toString();
			if (i >= start && i < end) {
				b.append(next);
				if (it.hasNext() && i + 1 < end) {
					b.append(delimiter);
				}
			}
			i++;
		}
		return b.toString();
	}

	public static String join(final Object[] array, final String separator) {
		if (array == null) {
			return null;
		}
		return join(array, separator, 0, array.length);
	}

	public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
		if (array == null) {
			return null;
		}
		if (separator == null) {
			separator = "";
		}
		final int noOfItems = endIndex - startIndex;
		if (noOfItems <= 0) {
			return "";
		}
		final StringBuilder buf = new StringBuilder(noOfItems * 16);
		for (int i = startIndex; i < endIndex; ++i) {
			if (i > startIndex) {
				buf.append(separator);
			}
			if (array[i] != null) {
				buf.append(array[i]);
			}
		}
		return buf.toString();
	}

	public static String toHex(final String arg) {
		return String.format("%x", new BigInteger(1, arg.getBytes(/* YOUR_CHARSET? */)));
	}

	public static String toHex_Unsigned(final String arg) {
		final StringBuilder sb = new StringBuilder();
		for (final char c : arg.toCharArray()) {
			sb.append(String.format("%02x", ByteUtil.unsignedToBytes((byte) c)));
		}
		return sb.toString();
	}

	public static String onlyNumbers(final String string) {
		if (string == null) {
			return "";
		}
		final StringBuilder sb = new StringBuilder();
		for (final char c : string.toCharArray()) {
			if (Character.isDigit(c)) {
				sb.append(c);
			}
		}
		return sb.toString();
	}

	public static String onlyNumbersSpaces(final String string) {
		if (string == null) {
			return "";
		}
		final StringBuilder sb = new StringBuilder();
		for (final char c : string.toCharArray()) {
			if (Character.isDigit(c) || Character.isSpaceChar(c)) {
				sb.append(c);
			}
		}
		return sb.toString();
	}

	public static boolean isNumber(final String r) {
		for (final char c : r.toCharArray()) {
			if (!Character.isDigit(c) && c != '.')
				return false;
		}
		return true;
	}

	public static final String unescapeHtml3(final String input) {
		if (input == null)
			return null;
		StringWriter writer = null;
		int len = input.length();
		int i = 1;
		int st = 0;
		while (true) {
			// look for '&'
			while (i < len && input.charAt(i - 1) != '&')
				i++;
			if (i >= len)
				break;

			// found '&', look for ';'
			int j = i;
			while (j < len && j < i + MAX_ESCAPE + 1 && input.charAt(j) != ';')
				j++;
			if (j == len || j < i + MIN_ESCAPE || j == i + MAX_ESCAPE + 1) {
				i++;
				continue;
			}

			// found escape
			if (input.charAt(i) == '#') {
				// numeric escape
				int k = i + 1;
				int radix = 10;

				final char firstChar = input.charAt(k);
				if (firstChar == 'x' || firstChar == 'X') {
					k++;
					radix = 16;
				}

				try {
					int entityValue = Integer.parseInt(input.substring(k, j), radix);

					if (writer == null)
						writer = new StringWriter(input.length());
					writer.append(input.substring(st, i - 1));

					if (entityValue > 0xFFFF) {
						final char[] chrs = Character.toChars(entityValue);
						writer.write(chrs[0]);
						writer.write(chrs[1]);
					} else {
						writer.write(entityValue);
					}

				} catch (NumberFormatException ex) {
					i++;
					continue;
				}
			} else {
				// named escape
				CharSequence value = lookupMap.get(input.substring(i, j));
				if (value == null) {
					i++;
					continue;
				}

				if (writer == null)
					writer = new StringWriter(input.length());
				writer.append(input.substring(st, i - 1));

				writer.append(value);
			}

			// skip escape
			st = j + 1;
			i = st;
		}

		if (writer != null) {
			writer.append(input.substring(st, len));
			return writer.toString();
		}
		return input;
	}

	private static final String[][] ESCAPES =
		{
				{ "\"", "quot" }, // " - double-quote
				{ "&", "amp" }, // & - ampersand
				{ "<", "lt" }, // < - less-than
				{ ">", "gt" }, // > - greater-than

				// Mapping to escape ISO-8859-1 characters to their named HTML 3.x equivalents.
				{ "\u00A0", "nbsp" }, // non-breaking space
				{ "\u00A1", "iexcl" }, // inverted exclamation mark
				{ "\u00A2", "cent" }, // cent sign
				{ "\u00A3", "pound" }, // pound sign
				{ "\u00A4", "curren" }, // currency sign
				{ "\u00A5", "yen" }, // yen sign = yuan sign
				{ "\u00A6", "brvbar" }, // broken bar = broken vertical bar
				{ "\u00A7", "sect" }, // section sign
				{ "\u00A8", "uml" }, // diaeresis = spacing diaeresis
				{ "\u00A9", "copy" }, // © - copyright sign
				{ "\u00AA", "ordf" }, // feminine ordinal indicator
				{ "\u00AB", "laquo" }, // left-pointing double angle quotation mark = left pointing guillemet
				{ "\u00AC", "not" }, // not sign
				{ "\u00AD", "shy" }, // soft hyphen = discretionary hyphen
				{ "\u00AE", "reg" }, // ® - registered trademark sign
				{ "\u00AF", "macr" }, // macron = spacing macron = overline = APL overbar
				{ "\u00B0", "deg" }, // degree sign
				{ "\u00B1", "plusmn" }, // plus-minus sign = plus-or-minus sign
				{ "\u00B2", "sup2" }, // superscript two = superscript digit two = squared
				{ "\u00B3", "sup3" }, // superscript three = superscript digit three = cubed
				{ "\u00B4", "acute" }, // acute accent = spacing acute
				{ "\u00B5", "micro" }, // micro sign
				{ "\u00B6", "para" }, // pilcrow sign = paragraph sign
				{ "\u00B7", "middot" }, // middle dot = Georgian comma = Greek middle dot
				{ "\u00B8", "cedil" }, // cedilla = spacing cedilla
				{ "\u00B9", "sup1" }, // superscript one = superscript digit one
				{ "\u00BA", "ordm" }, // masculine ordinal indicator
				{ "\u00BB", "raquo" }, // right-pointing double angle quotation mark = right pointing guillemet
				{ "\u00BC", "frac14" }, // vulgar fraction one quarter = fraction one quarter
				{ "\u00BD", "frac12" }, // vulgar fraction one half = fraction one half
				{ "\u00BE", "frac34" }, // vulgar fraction three quarters = fraction three quarters
				{ "\u00BF", "iquest" }, // inverted question mark = turned question mark
				{ "\u00C0", "Agrave" }, // А - uppercase A, grave accent
				{ "\u00C1", "Aacute" }, // Б - uppercase A, acute accent
				{ "\u00C2", "Acirc" }, // В - uppercase A, circumflex accent
				{ "\u00C3", "Atilde" }, // Г - uppercase A, tilde
				{ "\u00C4", "Auml" }, // Д - uppercase A, umlaut
				{ "\u00C5", "Aring" }, // Е - uppercase A, ring
				{ "\u00C6", "AElig" }, // Ж - uppercase AE
				{ "\u00C7", "Ccedil" }, // З - uppercase C, cedilla
				{ "\u00C8", "Egrave" }, // И - uppercase E, grave accent
				{ "\u00C9", "Eacute" }, // Й - uppercase E, acute accent
				{ "\u00CA", "Ecirc" }, // К - uppercase E, circumflex accent
				{ "\u00CB", "Euml" }, // Л - uppercase E, umlaut
				{ "\u00CC", "Igrave" }, // М - uppercase I, grave accent
				{ "\u00CD", "Iacute" }, // Н - uppercase I, acute accent
				{ "\u00CE", "Icirc" }, // О - uppercase I, circumflex accent
				{ "\u00CF", "Iuml" }, // П - uppercase I, umlaut
				{ "\u00D0", "ETH" }, // Р - uppercase Eth, Icelandic
				{ "\u00D1", "Ntilde" }, // С - uppercase N, tilde
				{ "\u00D2", "Ograve" }, // Т - uppercase O, grave accent
				{ "\u00D3", "Oacute" }, // У - uppercase O, acute accent
				{ "\u00D4", "Ocirc" }, // Ф - uppercase O, circumflex accent
				{ "\u00D5", "Otilde" }, // Х - uppercase O, tilde
				{ "\u00D6", "Ouml" }, // Ц - uppercase O, umlaut
				{ "\u00D7", "times" }, // multiplication sign
				{ "\u00D8", "Oslash" }, // Ш - uppercase O, slash
				{ "\u00D9", "Ugrave" }, // Щ - uppercase U, grave accent
				{ "\u00DA", "Uacute" }, // Ъ - uppercase U, acute accent
				{ "\u00DB", "Ucirc" }, // Ы - uppercase U, circumflex accent
				{ "\u00DC", "Uuml" }, // Ь - uppercase U, umlaut
				{ "\u00DD", "Yacute" }, // Э - uppercase Y, acute accent
				{ "\u00DE", "THORN" }, // Ю - uppercase THORN, Icelandic
				{ "\u00DF", "szlig" }, // Я - lowercase sharps, German
				{ "\u00E0", "agrave" }, // а - lowercase a, grave accent
				{ "\u00E1", "aacute" }, // б - lowercase a, acute accent
				{ "\u00E2", "acirc" }, // в - lowercase a, circumflex accent
				{ "\u00E3", "atilde" }, // г - lowercase a, tilde
				{ "\u00E4", "auml" }, // д - lowercase a, umlaut
				{ "\u00E5", "aring" }, // е - lowercase a, ring
				{ "\u00E6", "aelig" }, // ж - lowercase ae
				{ "\u00E7", "ccedil" }, // з - lowercase c, cedilla
				{ "\u00E8", "egrave" }, // и - lowercase e, grave accent
				{ "\u00E9", "eacute" }, // й - lowercase e, acute accent
				{ "\u00EA", "ecirc" }, // к - lowercase e, circumflex accent
				{ "\u00EB", "euml" }, // л - lowercase e, umlaut
				{ "\u00EC", "igrave" }, // м - lowercase i, grave accent
				{ "\u00ED", "iacute" }, // н - lowercase i, acute accent
				{ "\u00EE", "icirc" }, // о - lowercase i, circumflex accent
				{ "\u00EF", "iuml" }, // п - lowercase i, umlaut
				{ "\u00F0", "eth" }, // р - lowercase eth, Icelandic
				{ "\u00F1", "ntilde" }, // с - lowercase n, tilde
				{ "\u00F2", "ograve" }, // т - lowercase o, grave accent
				{ "\u00F3", "oacute" }, // у - lowercase o, acute accent
				{ "\u00F4", "ocirc" }, // ф - lowercase o, circumflex accent
				{ "\u00F5", "otilde" }, // х - lowercase o, tilde
				{ "\u00F6", "ouml" }, // ц - lowercase o, umlaut
				{ "\u00F7", "divide" }, // division sign
				{ "\u00F8", "oslash" }, // ш - lowercase o, slash
				{ "\u00F9", "ugrave" }, // щ - lowercase u, grave accent
				{ "\u00FA", "uacute" }, // ъ - lowercase u, acute accent
				{ "\u00FB", "ucirc" }, // ы - lowercase u, circumflex accent
				{ "\u00FC", "uuml" }, // ь - lowercase u, umlaut
				{ "\u00FD", "yacute" }, // э - lowercase y, acute accent
				{ "\u00FE", "thorn" }, // ю - lowercase thorn, Icelandic
				{ "\u00FF", "yuml" }, // я - lowercase y, umlaut
		};

	private static final int MIN_ESCAPE = 2;
	private static final int MAX_ESCAPE = 6;

	private static final Map<String, CharSequence> lookupMap;
	static {
		lookupMap = FastMaps.map(ESCAPES.length);
		for (final CharSequence[] seq : ESCAPES)
			lookupMap.put(seq[1].toString(), seq[0]);
	}

	public static boolean isZeroLengthOrNull(final StringBuilder input) {
		return (input == null || input.length() == 0 || input.toString().isEmpty());
	}

	public static boolean isZeroLengthOrNull(final String input) {
		return (input == null || input.isEmpty());
	}

	public static String defaultString(final String s) {
		if (s == null)
			return "";
		return s;
	}

	public static String reverse(final String orig) {
		char[] s = orig.toCharArray();
		int n = s.length;
		int halfLength = n / 2;
		for (int i = 0; i < halfLength; i++) {
			char temp = s[i];
			s[i] = s[n - 1 - i];
			s[n - 1 - i] = temp;
		}
		return new String(s);
	}

	public static Iterable<MatchResult> allMatches(final Pattern p, final CharSequence input) {
		return new Iterable<MatchResult>() {
			public Iterator<MatchResult> iterator() {
				return new Iterator<MatchResult>() {
					// Use a matcher internally.
					final Matcher matcher = p.matcher(input);
					// Keep a match around that supports any interleaving of hasNext/next calls.
					MatchResult pending;

					public boolean hasNext() {
						// Lazily fill pending, and avoid calling find() multiple times if the
						// clients call hasNext() repeatedly before sampling via next().
						if (pending == null && matcher.find()) {
							pending = matcher.toMatchResult();
						}
						return pending != null;
					}

					public MatchResult next() {
						// Fill pending if necessary (as when clients call next() without
						// checking hasNext()), throw if not possible.
						if (!hasNext()) {
							throw new NoSuchElementException();
						}
						// Consume pending so next call to hasNext() does a find().
						MatchResult next = pending;
						pending = null;
						return next;
					}

					/** Required to satisfy the interface, but unsupported. */
					public void remove() {
						throw new UnsupportedOperationException();
					}
				};
			}
		};
	}

	public static Integer asIntegerNumber(final String variable) {
		final Number num = asNumber(variable);
		if (num instanceof Integer) {
			return (Integer) num;
		} else if (num instanceof Double) {
			return (int) (double) num;
		} else {
			return (int) num;
		}
	}

	public static Number asNumber(final String variable) {
		if (variable == null || variable.isEmpty())
			throw new NullPointerException("variable == null!");
		if (variable.contains("."))
			return Double.parseDouble(variable);
		try {
			if (variable.length() > 9)
				return Long.parseLong(variable);
			return Integer.parseInt(variable);
		} catch (Exception e) {
			return Long.parseLong(variable);
		}
	}

	public static Number asNumber(final Number variable) {
		if (variable == null)
			throw new NullPointerException("variable == null!");
		final String x = String.valueOf(variable.toString().contains(".") ? variable.doubleValue() : variable.longValue());
		if (x.contains("."))
			return Double.parseDouble(x);
		return Integer.parseInt(x);
	}

	public static boolean isString(final String string) {
		return string.startsWith("\"") && string.endsWith("\"");
	}

	public static String replace(String source, final String os, final String ns) {
		if (source == null || source.isEmpty())
			return null;
		int i = 0;
		if ((i = source.indexOf(os, i)) >= 0) {
			final char[] sourceArray = source.toCharArray();
			final char[] nsArray = ns.toCharArray();
			int oLength = os.length();
			final StringBuilder buf = new StringBuilder(sourceArray.length);
			buf.append(sourceArray, 0, i).append(nsArray);
			i += oLength;
			int j = i;
			while ((i = source.indexOf(os, i)) > 0) {
				buf.append(sourceArray, j, i - j).append(nsArray);
				i += oLength;
				j = i;
			}
			buf.append(sourceArray, j, sourceArray.length - j);
			source = buf.toString();
			buf.setLength(0);
		}
		return source;
	}

	public static String replaceAll(String source, final Map<String, String> str) {
		if (str == null || str.isEmpty())
			return source;
		String out = source;
		for (final Map.Entry<String, String> _str : str.entrySet()) {
			out = replace(out, _str.getKey(), _str.getValue());
		}
		return out;
	}

	public static String replaceEach(final String text, final String[] searchList, final String[] replacementList) {
		return replaceEach(text, searchList, replacementList, false, 0);
	}

	private static String replaceEach(final String text, final String[] searchList, final String[] replacementList, final boolean repeat,
			final int timeToLive) {

		if (timeToLive < 0) {
			final Set<String> searchSet = new HashSet<String>(Arrays.asList(searchList));
			final Set<String> replacementSet = new HashSet<String>(Arrays.asList(replacementList));
			searchSet.retainAll(replacementSet);
			if (!searchSet.isEmpty()) {
				throw new IllegalStateException("Aborting to protect against StackOverflowError - " + "output of one loop is the input of another");
			}
		}

		if (isZeroLengthOrNull(text) || ArrayUtils.isEmpty(searchList) || ArrayUtils.isEmpty(replacementList)
				|| (ArrayUtils.isNotEmpty(searchList) && timeToLive == -1)) {
			return text;
		}

		final int searchLength = searchList.length;
		final int replacementLength = replacementList.length;

		if (searchLength != replacementLength) {
			throw new IllegalArgumentException("search and replace array lengths don't match: " + searchLength + " vs " + replacementLength);
		}

		final boolean[] noMoreMatchesForReplIndex = new boolean[searchLength];

		int textIndex = -1;
		int replaceIndex = -1;
		int tempIndex = -1;

		for (int i = 0; i < searchLength; i++) {
			if (noMoreMatchesForReplIndex[i] || isZeroLengthOrNull(searchList[i]) || replacementList[i] == null) {
				continue;
			}
			tempIndex = text.indexOf(searchList[i]);

			if (tempIndex == -1) {
				noMoreMatchesForReplIndex[i] = true;
			} else if (textIndex == -1 || tempIndex < textIndex) {
				textIndex = tempIndex;
				replaceIndex = i;
			}
		}

		if (textIndex == -1) {
			return text;
		}

		int start = 0;

		int increase = 0;

		for (int i = 0; i < searchList.length; i++) {
			if (searchList[i] == null || replacementList[i] == null) {
				continue;
			}
			final int greater = replacementList[i].length() - searchList[i].length();
			if (greater > 0) {
				increase += 3 * greater;
			}
		}
		increase = Math.min(increase, text.length() / 5);

		final StringBuilder buf = new StringBuilder(text.length() + increase);

		while (textIndex != -1) {

			for (int i = start; i < textIndex; i++) {
				buf.append(text.charAt(i));
			}
			buf.append(replacementList[replaceIndex]);

			start = textIndex + searchList[replaceIndex].length();

			textIndex = -1;
			replaceIndex = -1;

			for (int i = 0; i < searchLength; i++) {
				if (noMoreMatchesForReplIndex[i] || searchList[i] == null || searchList[i].isEmpty() || replacementList[i] == null) {
					continue;
				}
				tempIndex = text.indexOf(searchList[i], start);

				if (tempIndex == -1) {
					noMoreMatchesForReplIndex[i] = true;
				} else if (textIndex == -1 || tempIndex < textIndex) {
					textIndex = tempIndex;
					replaceIndex = i;
				}
			}

		}
		final int textLength = text.length();
		for (int i = start; i < textLength; i++) {
			buf.append(text.charAt(i));
		}
		final String result = buf.toString();
		if (!repeat) {
			return result;
		}

		return replaceEach(result, searchList, replacementList, repeat, timeToLive - 1);
	}

	private static final String CACHED_PATTERN = Pattern.compile("\\s").pattern();

	public static String cleanInvalidCharacters(final String in) {
		if (in == null)
			return null;
		else if (in.isEmpty())
			return in;
		final StringBuilder out = new StringBuilder(in.length());
		char current;
		if (in == null || in.isEmpty())
			return in;
		for (int i = 0; i < in.length(); i++) {
			current = in.charAt(i);
			if ((current == 0x9) || (current == 0xA) || (current == 0xD) || ((current >= 0x20) && (current <= 0xD7FF))
					|| ((current >= 0xE000) && (current <= 0xFFFD)) || ((current >= 0x10000) && (current <= 0x10FFFF))) {
				out.append(current);
			}

		}
		return out.toString().replaceAll(CACHED_PATTERN, " ");
	}

	private static final boolean USE_CHAR_ARR_STREAMING = false;

	public static String[] charArrToString(final String str) {
		if (str == null || str.isEmpty())
			return new String[0];
		if (USE_CHAR_ARR_STREAMING) {
			return str.codePoints().mapToObj(cp -> new String(Character.toChars(cp))).toArray(size -> new String[size]);
		} else {
			final String[] o = new String[str.length()];
			final char[] ca = str.toCharArray();
			for (int i = 0; i < ca.length; i++) {
				final char c = ca[i];
				o[i] = Character.toString(c);
			}
			return o;
		}
	}

	public static boolean containsEither(final String input, final String... words) {
		if (input == null || words == null)
			return false;
		if (input.isEmpty() || words.length == 0)
			return false;
		return Arrays.stream(words).anyMatch(input::contains);
	}

	public static boolean containsAll(final String input, final String... words) {
		if (input == null || words == null)
			return false;
		if (input.isEmpty() || words.length == 0)
			return false;
		return Arrays.stream(words).allMatch(input::contains);
	}

	public static boolean startsWithEither(final String input, final String... words) {
		if (input == null || words == null)
			return false;
		if (input.isEmpty() || words.length == 0)
			return false;
		return Arrays.stream(words).anyMatch(input::startsWith);
	}

	public static boolean isEqual(final String[] data, final int index, final String search) {
		try {
			return data[index].equals(search);
		} catch (Exception e) {
			return false;
		}
	}

	public static String filterStringS(final String data, final int max) {
		final variable<Integer> found = new variable<Integer>(0);
		final StringBuilder builder = new StringBuilder();
		for (final char f : data.toCharArray()) {
			if (f == ' ') {
				found.setObject(found.getObject() + 1);
			} else {
				found.setObject(0);
			}
			if (found.getObject() > max) {
				break;
			}
			if (f == data.charAt(data.length() - 1) && f == ' ') {
				break;
			} else if (f != '\u0000') {
				builder.append(f);
			}
		}
		return builder.toString();
	}

	public static String toBinary(final String text) {
		if (text == null || text.isEmpty())
			return null;
		final StringBuilder sb = new StringBuilder();
		for (char character : text.toCharArray()) {
			sb.append(Integer.toBinaryString(character));
		}
		return sb.toString();
	}

	public static String binaryToText(String binaryString) {
		if (binaryString == null || binaryString.isEmpty())
			return null;
		final StringBuilder stringBuilder = new StringBuilder();
		int charCode;
		for (int i = 0; i < binaryString.length(); i += 8) {
			charCode = Integer.parseInt(binaryString.substring(i, i + 8), 2);
			String returnChar = Character.toString((char) charCode);
			stringBuilder.append(returnChar);
		}
		return stringBuilder.toString();
	}

	public static String repeat(final String str, final int times) {
		return Stream.generate(() -> str).limit(times).collect(Collectors.joining());
	}

	public static int hash8(final String val) {
		return hash8(val.getBytes(CharsetUtil.UTF_8));
	}

	public static int hash8(final Object val) {
		return hash8(String.valueOf(val));
	}

	public static int hash8(final byte[] val) {
		if (val == null || val.length == 0)
			return 0;
		int h = 1, i = 0;
		for (; i + 7 < val.length; i += 8) {
			h = 31 * 31 * 31 * 31 * 31 * 31 * 31 * 31 * h + 31 * 31 * 31 * 31 * 31 * 31 * 31 * val[i] + 31 * 31 * 31 * 31 * 31 * 31 * val[i + 1]
					+ 31 * 31 * 31 * 31 * 31 * val[i + 2] + 31 * 31 * 31 * 31 * val[i + 3] + 31 * 31 * 31 * val[i + 4] + 31 * 31 * val[i + 5] + 31 * val[i + 6]
					+ val[i + 7];
		}
		for (; i + 3 < val.length; i += 4) {
			h = 31 * 31 * 31 * 31 * h + 31 * 31 * 31 * val[i] + 31 * 31 * val[i + 1] + 31 * val[i + 2] + val[i + 3];
		}
		for (; i < val.length; i++) {
			h = 31 * h + val[i];
		}
		return h;
	}

	public static boolean isAllASCII(final String input) {
		boolean isASCII = true;
		for (int i = 0; i < input.length(); i++) {
			int c = input.charAt(i);
			if (c > 0x7F) {
				isASCII = false;
				break;
			}
		}
		return isASCII;
	}

	/* /// BACKPORT FOR String.isBlank() /// */

	public static int indexOfNonWhitespace(byte[] value) {
		int length = value.length >> 1;
		int left = 0;
		while (left < length) {
			int codepoint = codePointAt(value, left, length);
			if (codepoint != ' ' && codepoint != '\t' && !Character.isWhitespace(codepoint)) {
				break;
			}
			left += Character.charCount(codepoint);
		}
		return left;
	}

	public static int length(byte[] value) {
		return value.length >> 1;
	}

	static final int HI_BYTE_SHIFT;
	static final int LO_BYTE_SHIFT;
	static {
		if (JVM.isBigEndian()) {
			HI_BYTE_SHIFT = 8;
			LO_BYTE_SHIFT = 0;
		} else {
			HI_BYTE_SHIFT = 0;
			LO_BYTE_SHIFT = 8;
		}
	}

	static char getChar(byte[] val, int index) {
		assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
		index <<= 1;
		return (char) (((val[index++] & 0xff) << HI_BYTE_SHIFT) | ((val[index] & 0xff) << LO_BYTE_SHIFT));
	}

	private static String outOfBoundsMessage(String checkKind, List<? extends Number> args) {
		if (checkKind == null && args == null) {
			return String.format("Range check failed");
		} else if (checkKind == null) {
			return String.format("Range check failed: %s", args);
		} else if (args == null) {
			return String.format("Range check failed: %s", checkKind);
		}

		int argSize = 0;
		switch (checkKind) {
			case "checkIndex":
				argSize = 2;
				break;
			case "checkFromToIndex":
			case "checkFromIndexSize":
				argSize = 3;
				break;
			default:
		}

		// Switch to default if fewer or more arguments than required are supplied
		switch ((args.size() != argSize) ? "" : checkKind) {
			case "checkIndex":
				return String.format("Index %s out of bounds for length %s", args.get(0), args.get(1));
			case "checkFromToIndex":
				return String.format("Range [%s, %s) out of bounds for length %s", args.get(0), args.get(1), args.get(2));
			case "checkFromIndexSize":
				return String.format("Range [%s, %<s + %s) out of bounds for length %s", args.get(0), args.get(1), args.get(2));
			default:
				return String.format("Range check failed: %s %s", checkKind, args);
		}
	}

	private static RuntimeException outOfBounds(BiFunction<String, List<Number>, ? extends RuntimeException> oobef, String checkKind, Number... args) {
		List<Number> largs = Lists.of(args);
		RuntimeException e = oobef == null ? null : oobef.apply(checkKind, largs);
		return e == null ? new IndexOutOfBoundsException(outOfBoundsMessage(checkKind, largs)) : e;
	}

	private static RuntimeException outOfBoundsCheckIndex(BiFunction<String, List<Number>, ? extends RuntimeException> oobe, int index, int length) {
		return outOfBounds(oobe, "checkIndex", index, length);
	}

	public static <X extends RuntimeException> int checkIndex(int index, int length, BiFunction<String, List<Number>, X> oobef) {
		if (index < 0 || index >= length)
			throw outOfBoundsCheckIndex(oobef, index, length);
		return index;
	}

	public static void checkIndex(int off, byte[] val) {
		checkIndex(off, length(val), SIOOBE_FORMATTER);
	}

	public static <X extends RuntimeException> BiFunction<String, List<Number>, X> outOfBoundsExceptionFormatter(Function<String, X> f) {
		// Use anonymous class to avoid bootstrap issues if this method is
		// used early in startup
		return new BiFunction<String, List<Number>, X>() {
			@Override
			public X apply(String checkKind, List<Number> args) {
				return f.apply(outOfBoundsMessage(checkKind, args));
			}
		};
	}

	public static final BiFunction<String, List<Number>, StringIndexOutOfBoundsException> SIOOBE_FORMATTER = outOfBoundsExceptionFormatter(
			new Function<String, StringIndexOutOfBoundsException>() {
				@Override
				public StringIndexOutOfBoundsException apply(String s) {
					return new StringIndexOutOfBoundsException(s);
				}
			});

	private static int codePointAt(byte[] value, int index, int end, boolean checked) {
		assert index < end;
		if (checked) {
			checkIndex(index, value);
		}
		char c1 = getChar(value, index);
		if (Character.isHighSurrogate(c1) && ++index < end) {
			if (checked) {
				checkIndex(index, value);
			}
			char c2 = getChar(value, index);
			if (Character.isLowSurrogate(c2)) {
				return Character.toCodePoint(c1, c2);
			}
		}
		return c1;
	}

	public static int codePointAt(byte[] value, int index, int end) {
		return codePointAt(value, index, end, false /* unchecked */);
	}

	public static boolean isBlank(final String output) {
		if (output == null || output.isEmpty())
			return true;
		return indexOfNonWhitespace(output.getBytes()) == output.length();
	}

	public static String stripLeading(byte[] value) {
		int length = value.length >>> 1;
		int left = indexOfNonWhitespace(value);
		return (left != 0) ? newString(value, left, length - left) : null;
	}

	public static String newString(byte[] val, int index, int len) {
		if (len == 0) {
			return "";
		}

		int last = index + len;
		return new String(Arrays.copyOfRange(val, index << 1, last << 1), CharsetUtil.UTF_16);
	}

	public static String stripLeading(final String value) {
		return stripLeading(value.getBytes(CharsetUtil.UTF_16));
	}

	public static String simpleClassName(final Object obj) {
		return obj == null ? "null" : obj.getClass().getSimpleName();
	}

	public static boolean equals(final CharSequence cs1, final CharSequence cs2) {
		if (cs1 == cs2) {
			return true;
		}
		if (cs1 == null || cs2 == null) {
			return false;
		}
		if (cs1 instanceof String && cs2 instanceof String) {
			return cs1.equals(cs2);
		}
		return cs1.length() == cs2.length() && regionMatches(cs1, false, 0, cs2, 0, cs1.length());
	}

	public static boolean regionMatches(final CharSequence cs, final boolean ignoreCase, final int thisStart, final CharSequence substring, final int start,
			final int length) {
		if (cs instanceof String && substring instanceof String) {
			return ((String) cs).regionMatches(ignoreCase, thisStart, (String) substring, start, length);
		}
		int index1 = thisStart;
		int index2 = start;
		int tmpLen = length;
		final int srcLen = cs.length() - thisStart;
		final int otherLen = substring.length() - start;
		if (thisStart < 0 || start < 0 || length < 0) {
			return false;
		}
		if (srcLen < length || otherLen < length) {
			return false;
		}
		while (tmpLen-- > 0) {
			final char c1 = cs.charAt(index1++);
			final char c2 = substring.charAt(index2++);
			if (c1 == c2) {
				continue;
			}
			if (!ignoreCase) {
				return false;
			}
			if (Character.toUpperCase(c1) != Character.toUpperCase(c2) && Character.toLowerCase(c1) != Character.toLowerCase(c2)) {
				return false;
			}
		}
		return true;
	}

	private static boolean hasLength(final String str) {
		return (str != null && str.length() > 0);
	}

	public static String trimAllWhitespace(final String str) {
		if (!hasLength(str)) {
			return str;
		}

		final int len = str.length();
		final StringBuilder sb = new StringBuilder(str.length());
		for (int i = 0; i < len; i++) {
			char c = str.charAt(i);
			if (!Character.isWhitespace(c)) {
				sb.append(c);
			}
		}
		return sb.toString();
	}

	/* */

	private static final int INDEX_NOT_FOUND = -1;

	static int indexOf(final CharSequence cs, final CharSequence searchChar, final int start) {
		if (cs instanceof String) {
			return ((String) cs).indexOf(searchChar.toString(), start);
		}
		if (cs instanceof StringBuilder) {
			return ((StringBuilder) cs).indexOf(searchChar.toString(), start);
		}
		if (cs instanceof StringBuffer) {
			return ((StringBuffer) cs).indexOf(searchChar.toString(), start);
		}
		return cs.toString().indexOf(searchChar.toString(), start);
	}

	public static boolean isEmpty(final CharSequence cs) {
		return cs == null || cs.length() == 0;
	}

	public static int countMatches(final CharSequence str, final CharSequence sub) {
		if (isEmpty(str) || isEmpty(sub)) {
			return 0;
		}
		int count = 0;
		int idx = 0;
		while ((idx = indexOf(str, sub, idx)) != INDEX_NOT_FOUND) {
			count++;
			idx += sub.length();
		}
		return count;
	}

	public static boolean hasOnlyNumbers(final String string) {
		if (StringsUtil.isBlank(string))
			return false;

		return string.matches("[0-9]+");
	}

	/* */

}
